package com.hongyi.common.mybatis.interceptor;

import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.hongyi.common.framework.utils.common.ArrayUtils;
import com.hongyi.common.mybatis.annotation.DataColumn;
import com.hongyi.common.mybatis.handler.DataPermissionHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import net.sf.jsqlparser.statement.select.SetOperationList;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 数据权限拦截器
 *
 * <p>在执行SQL查询或更新之前,根据如数据权限注解来修改 SQL 语句</p>
 *<pre>
 *     1.实现InnerInterceptor接口,在执行查询,修改和删除之前验证是否应用权限注解,并修改相关sql;
 *     2.集成抽象类JsqlParserSupport,重写相关方法修改具体的sql修改逻辑
 *</pre>
 * @author DingHao
 */
public class DataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {

    //创建数据权限过滤处理器对象
    private final DataPermissionHandler dataPermissionHandler = new DataPermissionHandler();

    /**
     * 无效注解方法缓存用于快速返回
     * <p>使用线程安全的ConcurrentHashMap存储那些已经检查过没有数据权限注解的方法ID,以避免重复检查,提高性能</p>
     */
    //private final Set<String> invalidCacheSet = new ConcurrentHashSet<>();
    private final ConcurrentHashMap<String, Boolean> invalidCacheMap = new ConcurrentHashMap<>();

    /**
     * 查询前将添加必要的数据权限条件到SQL中
     * <pre>
     *     1.在MyBatis准备执行SQ 查询语句之前被调用;
     *     2.如果查询语句需要验证数据权限,则解析原始SQL,并使用parserSingle方法添加必要的数据权限条件到SQL中
     * </pre>
     * @param executor 执行器对象,负责执行 SQL 语句,可以通过这个对象来执行 SQL 语句、获取连接等
     * @param ms 即要执行的 SQL 语句,包括 SQL 语句本身、参数映射、结果映射
     * @param parameter 传递给 SQL 语句的参数对象。这个对象可以是任意类型，取决于你的 SQL 语句需要的参数
     * @param rowBounds 用于分页查询的行边界对象。包含分页的起始行号和行数限制
     * @param resultHandler  结果处理器对象，用于处理查询结果
     * @param boundSql 包含最终要执行的 SQL 语句及其参数信息。这是 MyBatis 在执行 SQL 语句之前生成的对象
     */
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        /**
         * 检查忽略注解
         * <pre>
         *     1.ms.getId()：获取当前 MappedStatement 的唯一标识符;
         *     2.检查当前要执行的 SQL 语句是否应该忽略数据权限控制
         * </pre>
         */
        if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
            return;
        }
        // 检查是否无效 无数据权限注解
        if (invalidCacheMap.containsKey(ms.getId())) {
            return;
        }
        //查找MyBatis执行查询方法上的数据权限注解
        DataColumn[] dataColumns = dataPermissionHandler.findAnnotation(ms.getId());
        //如果查询的注解是空的就存储到ConcurrentHashMap中,避免重复检查
        if (ArrayUtils.isEmpty(dataColumns)) {
            invalidCacheMap.put(ms.getId(), true);
            return;
        }
        // 解析 sql 分配对应方法,将 BoundSql 对象转换为 MPBoundSql 对象
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        //返回值是一个新的 SQL 语句字符串，包含了数据权限控制条件
        mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
    }

    /**
     * 修改和删除前将添加必要的数据权限条件到SQL中
     * <pre>
     *     1.在MyBatis准备执行SQ 修改和删除语句之前被调用;
     *     2.如果方法有效且存在数据权限注解，则调用 parserMulti方法来修改 SQL 语句
     * </pre>
     * @param sh 负责准备和执行 SQL 语句的处理器对象。你可以通过这个对象来获取和修改 SQL 语句
     * @param connection 数据库连接对象，用于与数据库进行交互
     * @param transactionTimeout 事务超时时间，单位为秒。如果事务在指定时间内未完成，将被自动回滚
     */
    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        //将 StatementHandler 对象转换为 MPStatementHandler 对象,以便进行更多的操作
        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        //获取当前要执行的 SQL 语句的 MappedStatement 对象，包含 SQL 语句的详细信息
        MappedStatement ms = mpSh.mappedStatement();
        //获取当前 SQL 语句的类型
        SqlCommandType sct = ms.getSqlCommandType();
        //当前 SQL 语句是 UPDATE 或 DELETE 类型
        if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
            //检查忽略注解
            if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
                return;
            }
            // 检查是否无效 无数据权限注解
            if (invalidCacheMap.containsKey(ms.getId())) {
                return;
            }
            //查找MyBatis执行查询方法上的数据权限注解
            DataColumn[] dataColumns = dataPermissionHandler.findAnnotation(ms.getId());
            //如果查询的注解是空的就存储到ConcurrentHashMap中,避免重复检查
            if (ArrayUtils.isEmpty(dataColumns)) {
                invalidCacheMap.put(ms.getId(), true);
                return;
            }
            //获取 MPBoundSql 对象，包含原始的 SQL 语句和其他相关信息
            PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
            //添加必要的数据权限控制条件,将修改后的 SQL 语句设置回 MPBoundSql 对象中
            mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
        }
    }

    /**
     * 重写processSelect方法,具体的执行
     * @param select 代表一个完整的 SELECT 语句对象。这个对象包含了整个查询的所有部分，包括 SELECT 子句、FROM 子句、WHERE 子句等
     * @param index 用于标识当前处理的 SQL 语句的位置或索引。在这个方法中，这个参数并没有被使用，可能是为了与其他方法保持一致或预留未来扩展
     * @param sql 原始的 SQL 语句文本
     * @param obj 通常用于传递额外的信息
     */
    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
        //获取 SELECT 语句的主体部分（SelectBody
        SelectBody selectBody = select.getSelectBody();
        //如果 SelectBody 是 PlainSelect 类型，表示这是一个普通的 SELECT 查询
        if (selectBody instanceof PlainSelect ) {
            PlainSelect plainSelect = (PlainSelect) selectBody;
            //强制转换后为PlainSelect 对象设置新的 WHERE 子句
            this.setWhere(plainSelect, (String) obj);
        //如果 SelectBody 是 SetOperationList 类型，表示这是一个包含多个子查询的复杂查询（例如 UNION、INTERSECT 等）
        } else if (selectBody instanceof SetOperationList) {
            SetOperationList setOperationList = (SetOperationList) selectBody;
            //强制转换后获取 SetOperationList 中的所有子查询（SelectBody 列表）
            List<SelectBody> selectBodyList = setOperationList.getSelects();
            //通过遍历对每个子查询调用 setWhere 方法，为每个子查询设置新的 WHERE 子句
            selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
        }
    }

    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
        if (null != sqlSegment) {
            update.setWhere(sqlSegment);
        }
    }

    @Override
    protected void processDelete(Delete delete, int index, String sql, Object obj) {
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
        if (null != sqlSegment) {
            delete.setWhere(sqlSegment);
        }
    }

    /**
     * 设置 where 条件
     *
     * @param plainSelect       查询对象
     * @param mappedStatementId 执行方法id
     */
    protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
        //数据过滤后可拼接的sql语句
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
        if (null != sqlSegment) {
            plainSelect.setWhere(sqlSegment);
        }
    }

}

